﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Windows.Storage;

namespace FreshMeat.Storage
{
   /// <summary>
   /// Windows Store App storage implementation of <see cref="IFileSystem"/>.
   /// </summary>
   public class StorageFileSystem : IFileSystem
   {
      readonly StorageFolder root;

      /// <summary>
      /// Creates instance using the specified parameters.
      /// </summary>
      /// <param name="root">Folder to use as the "root" of this file system.</param>
      public StorageFileSystem(StorageFolder root)
      {
         if (root == null)
            throw new ArgumentNullException("root");

         this.root = root;
      }

      /// <summary>
      /// The root folder.
      /// </summary>
      public StorageFolder Root { get { return root; } }

      /// <summary>
      /// Creates a folder.
      /// If the folder already exists, this does nothing.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      public void CreateFolder(string path)
      {
         throw new NotSupportedException();
      }

      /// <summary>
      /// Creates a folder.
      /// If the folder already exists, this does nothing.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      public async Task CreateFolderAsync(string path)
      {
         if (path == "")
            return;

         path = path.TrimEnd(Path.FolderSeparatorChar);
         var folderNames = path.Split(new[] {Path.FolderSeparatorChar});
         if (folderNames.Any(name => name == ""))
            throw new ArgumentException("One or more folders in the path are empty.", "path");

         var parent = root;
         foreach (var name in folderNames)
            parent = await parent.CreateFolderAsync(name, CreationCollisionOption.OpenIfExists);
      }

      /// <summary>
      /// Deletes a folder.
      /// If the folder does not exist, this does nothing.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      /// <returns>True if the file was deleted; false if the file did not exist.</returns>
      public bool DeleteFolder(string path)
      {
         throw new NotSupportedException();
      }

      /// <summary>
      /// Deletes a folder.
      /// If the folder does not exist, this does nothing.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      /// <returns>True if the file was deleted; false if the file did not exist.</returns>
      public async Task<bool> DeleteFolderAsync(string path)
      {
         try
         {
            var folder = await root.GetFolderAsync(path);
            await folder.DeleteAsync(StorageDeleteOption.Default);
            return true;
         }
         catch (FileNotFoundException)
         {
            return false;
         }
      }

      /// <summary>
      /// Determines whether a folder exists.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      public bool FolderExists(string path)
      {
         throw new NotSupportedException();
      }

      /// <summary>
      /// Determines whether a folder exists.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      public async Task<bool> FolderExistsAsync(string path)
      {
         if (path == "")
            return true;

         try
         {
            await root.GetFolderAsync(path);
            return true;
         }
         catch (FileNotFoundException)
         {
            return false;
         }
      }

      /// <summary>
      /// Enumerates the files in a folder.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      /// <param name="searchPattern">The search pattern, e.g. "*.txt" or "Doc?.txt.</param>
      /// <param name="recurse">True to recurse into subfolders, false to only check the specified folder.</param>
      /// <returns>Paths to files which match the search pattern.</returns>
      public IEnumerable<string> EnumerateFiles(string path, string searchPattern, bool recurse)
      {
         throw new NotSupportedException();
      }

      /// <summary>
      /// Enumerates the files in a folder.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      /// <param name="searchPattern">The search pattern, e.g. "*.txt" or "Doc?.txt.</param>
      /// <param name="recurse">True to recurse into subfolders, false to only check the specified folder.</param>
      /// <returns>Paths to files which match the search pattern.</returns>
      public async Task<IEnumerable<string>> EnumerateFilesAsync(string path, string searchPattern, bool recurse)
      {
         var emulator = new WildcardSearchEmulator(searchPattern);

         var files = new List<string>();

         var folder = path == "" ? root : await root.GetFolderAsync(path);
         await EnumerateFiles(folder, emulator, recurse, files);

         return files.ToArray();
      }

      async Task EnumerateFiles(StorageFolder folder, WildcardSearchEmulator emulator, bool recurse, List<string> accumulatedFiles)
      {
         var files = await folder.GetFilesAsync();
         var paths = files
            .Where(file => emulator.IsMatch(file.Name))
            .Select(file => file.Path.Substring(root.Path.Length + 1))
            .ToArray();

         accumulatedFiles.AddRange(paths);

         if (!recurse)
            return;

         var subfolders = await folder.GetFoldersAsync();
         foreach (var subfolder in subfolders)
            await EnumerateFiles(subfolder, emulator, true, accumulatedFiles);
      }

      /// <summary>
      /// Moves and/or renames a file.
      /// An exception will be thrown if the destination file already exists.
      /// </summary>
      /// <param name="sourcePath">The path to the source file.</param>
      /// <param name="destinationPath">The path to the destination file.</param>
      public void MoveFile(string sourcePath, string destinationPath)
      {
         throw new NotSupportedException();
      }

      /// <summary>
      /// Moves and/or renames a file.
      /// An exception will be thrown if the destination file already exists.
      /// </summary>
      /// <param name="sourcePath">The path to the source file.</param>
      /// <param name="destinationPath">The path to the destination file.</param>
      public async Task MoveFileAsync(string sourcePath, string destinationPath)
      {
         if (string.Compare(sourcePath, destinationPath, StringComparison.CurrentCultureIgnoreCase) == 0)
            return;

         StorageFile source;
         try
         {
            source = await root.GetFileAsync(sourcePath);
         }
         catch (FileNotFoundException)
         {
            throw new InvalidOperationException("Cannot move file because source doesn't exist.");
         }

         var destinationFolder = Path.GetLocation(destinationPath);
         var destination = await root.GetFolderAsync(destinationFolder);
         var destinationName = Path.GetName(destinationPath);

         try
         {
            await source.MoveAsync(destination, destinationName, NameCollisionOption.FailIfExists);
         }
         catch (Exception e)
         {
            if (e.HResult == -2147024713)
               throw new InvalidOperationException("Cannot move file because destination already exists.");
            throw;
         }
      }

      /// <summary>
      /// Determines whether a file exists.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      public bool FileExists(string path)
      {
         throw new NotSupportedException();
      }

      /// <summary>
      /// Determines whether a file exists.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      public async Task<bool> FileExistsAsync(string path)
      {
         try
         {
            await root.GetFileAsync(path);
            return true;
         }
         catch (FileNotFoundException)
         {
            return false;
         }
      }

      /// <summary>
      /// Deletes a file.
      /// If the file does not exist, this does nothing.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <returns>True if the file was deleted; false if the file did not exist.</returns>
      public bool DeleteFile(string path)
      {
         throw new NotSupportedException();
      }

      /// <summary>
      /// Deletes a file.
      /// If the file does not exist, this does nothing.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <returns>True if the file was deleted; false if the file did not exist.</returns>
      public async Task<bool> DeleteFileAsync(string path)
      {
         try
         {
            var file = await root.GetFileAsync(path);
            await file.DeleteAsync(StorageDeleteOption.Default);
            return true;
         }
         catch (FileNotFoundException)
         {
            return false;
         }
      }

      /// <summary>
      /// Writes text to a file.
      /// If the file does not exist then it's created; if it does exist then it's overwritten.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="text">The contents of the file.</param>
      public void WriteAllText(string path, string text)
      {
         throw new NotSupportedException();
      }

      /// <summary>
      /// Writes text to a file.
      /// If the file does not exist then it's created; if it does exist then it's overwritten.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="text">The contents of the file.</param>
      public async Task WriteAllTextAsync(string path, string text)
      {
         var file = await root.CreateFileAsync(path, CreationCollisionOption.ReplaceExisting);
       
         using (var stream = await file.OpenStreamForWriteAsync())
         using (var writer = new StreamWriter(stream))
            await writer.WriteAsync(text);
      }

      /// <summary>
      /// Reads the contents of an existing file as text.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <returns>The contents of the file.</returns>
      public string ReadAllText(string path)
      {
         throw new NotSupportedException();
      }

      /// <summary>
      /// Reads the contents of an existing file as text.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <returns>The contents of the file.</returns>
      public async Task<string> ReadAllTextAsync(string path)
      {
         var file = await root.GetFileAsync(path);

         using (var stream = await file.OpenStreamForReadAsync())
         using (var reader = new StreamReader(stream))
            return await reader.ReadToEndAsync();
      }

      /// <summary>
      /// Opens an existing file for reading.
      /// The sharing mode is read-only.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="mode">Open mode.</param>
      /// <param name="share">Share mode.</param>
      /// <returns>Stream that can be read from but not written to; must be disposed to close file.</returns>
      public Stream OpenForRead(string path, OpenMode mode = OpenMode.Open, OpenShare share = OpenShare.Read)
      {
         throw new NotSupportedException();
      }

      /// <summary>
      /// Opens an existing file for reading.
      /// The sharing mode is read-only.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="mode">Open mode.</param>
      /// <param name="share">Share mode.</param>
      /// <returns>Stream that can be read from but not written to; must be disposed to close file.</returns>
      public async Task<Stream> OpenForReadAsync(string path, OpenMode mode = OpenMode.Open, OpenShare share = OpenShare.Read)
      {
         StorageFile file;
         switch (mode)
         {
            case OpenMode.CreateNew:
               file = await root.CreateFileAsync(path, CreationCollisionOption.FailIfExists);
               break;
            case OpenMode.Create:
               file = await root.CreateFileAsync(path, CreationCollisionOption.ReplaceExisting);
               break;
            case OpenMode.Open:
               file = await root.GetFileAsync(path);
               break;
            case OpenMode.OpenOrCreate:
               file = await root.CreateFileAsync(path, CreationCollisionOption.OpenIfExists);
               break;
            default:
               throw new ArgumentOutOfRangeException("mode");
         }

         return await file.OpenStreamForReadAsync();
      }

      /// <summary>
      /// Opens a file for writing; if the file does not exist, it is created.
      /// The sharing mode is none.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="mode">Open mode.</param>
      /// <param name="share">Share mode.</param>
      /// <returns>Stream that can be read from and written to; must be disposed to close file.</returns>
      public Stream OpenForWrite(string path, OpenMode mode = OpenMode.OpenOrCreate, OpenShare share = OpenShare.None)
      {
         throw new NotSupportedException();
      }

      /// <summary>
      /// Opens a file for writing; if the file does not exist, it is created.
      /// The sharing mode is none.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="mode">Open mode.</param>
      /// <param name="share">Share mode.</param>
      /// <returns>Stream that can be read from and written to; must be disposed to close file.</returns>
      public async Task<Stream> OpenForWriteAsync(string path, OpenMode mode = OpenMode.OpenOrCreate, OpenShare share = OpenShare.None)
      {
         StorageFile file;
         switch (mode)
         {
            case OpenMode.CreateNew:
               file = await root.CreateFileAsync(path, CreationCollisionOption.FailIfExists);
               break;
            case OpenMode.Create:
               file = await root.CreateFileAsync(path, CreationCollisionOption.ReplaceExisting);
               break;
            case OpenMode.Open:
               file = await root.GetFileAsync(path);
               break;
            case OpenMode.OpenOrCreate:
               file = await root.CreateFileAsync(path, CreationCollisionOption.OpenIfExists);
               break;
            default:
               throw new ArgumentOutOfRangeException("mode");
         }

          return await file.OpenStreamForWriteAsync(); // provides read & write access
      }
   }
}
